﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace DDD.Core {
    /// <summary>
    /// シェーダー プログラム クラス
    /// </summary>
    /// <remarks>
    /// GPU に転送されてレンダリングに使用されるプログラムの事をシェーダーと呼びます｡
    /// <see cref="ShaderProgram"/> はこのシェーダーを表現したクラスです｡
    /// <para>
    /// <h3>シェーダー プログラム</h3>
    /// <img src="NotImplemented">シェーダーのブロック図</img>
    /// シェーダー プログラムは <see cref="VertexShader"/> と <see cref="FragmentShader"/> の2つの
    /// シェーダーから構成されます｡<see cref="VertexShader"/> は1頂点に月1回実行され､頂点の色を決定します｡
    /// トライアングルを構成する3頂点の色が決定されると､内部を線形補完しながらピクセル単位で塗りつぶされます｡
    /// これをラスタライズと呼びます｡ラスタライズは GPU のラスタライザーによって自動的に実行されます｡
    /// 出力それに続く <see cref="FragmentShader"/> で処理されフラグメントの色が決定されます｡
    /// このフラグメントはフレームバッファーに合成されて最終的なピクセル値(=色)になります｡
    /// シェーダーの実行中プログラム全体から参照される定数値を <c>Uniform</c> 変数と呼びます｡
    /// <see cref="VertexShader"/> の入力となる頂点データを <c>Attribute</c> 変数と呼びます｡
    /// フラグメントをフレームバッファーに合成する処理をコンポジットと呼び､<see cref="CompositeMode"/> が担当します｡
    /// ポリゴン全体の塗りつぶしの設定は <see cref="PolygonMode"/> が担当します｡
    /// シェーダーを実行する為には､
    /// </para>
    /// <list type="bullet">
    ///   <item><see cref="ShaderProgram"/> </item>
    ///   <item><c>Uniform</c> 変数</item>
    ///   <item><c>Attribute</c> 変数</item>
    /// </list>
    /// の3つを準備し､必要に応じて<see cref="CompositeMode"/> と <see cref="PolygonMode"/> の設定を変更する事です｡
    /// <para>
    /// <h3>シェーダー プログラムの準備</h3>
    /// シェーダーはソース(文字列)の状態でロードされ､コンパイルとリンクを行って初めて使用可能になります。
    /// 通常それらはGPU ドライバーが行い､OpenGLのコンテキストが必要です｡
    /// DDDは レンダリング コンテキスト フリーなシーン構築ができるように､
    /// コンテキスト依存部分は <see cref="DDD.Core"/> には含まれず拡張メソッドによって実装されています｡
    /// 従ってシェーダーのコンパイルとリンクとそれに続くプログラムの妥当性の検証は
    /// レンダリングの最終段階まで実行されません｡
    /// シェーダープログラムの妥当性を検証したい場合は、OpenGL コンテキストが有効な状態で
    /// 手動で拡張メソッドを呼び出してください。
    /// 詳しくは <see cref="ShaderProgramExtensions"/> を参照してください。
    /// コンパイルとリンクが成功するとシェーダー中で宣言された <c>Uniform</c> 変数と <c>Attribute</c> 変数の情報にアクセスできるようになります｡
    /// 
    /// </para>
    /// <para>
    /// <h3>シェーダー変数</h3>
    /// シェーダーがコンパイルとリンクが成功すると､シェーダーで宣言された <c>Uniform</c> 変数と <c>Attribute</c> 変数にアクセスできるようになります｡
    /// 両者をはまとめて <see cref="ShaderDeclaration"/> として取得可能です｡
    /// シェーダーが正常に動作するためには宣言された変数に(CPU 側から)正しい値をセットしてやる事が重要です｡
    /// 値の設定はプログラム中でいつでも行え､妥当性に関してはその時点では検証されず､
    /// レンダリング時に一括して解決されます(遅延判定)｡
    /// 
    /// <c>Uniform</c> 変数はシェーダーの実行中にプログラム全体から参照可能なグローバル変数です｡
    /// シェーダーに <c>Uniform</c> 変数をセットするには <see cref="UniformArray"/> オブジェクトを <see cref="ShaderProgram"/> オブジェクトにセットします｡
    /// <c>Unofrm</c> 変数の詳細については､<see cref="UniformArray"/> の解説を参照してください｡
    /// 
    /// 
    /// <c>Attribute</c> 変数はシェーダーの入力として供給する頂点データです｡
    /// シェーダーに <c>Attribute</c> 変数をセットするには <see cref="VertexBuffer"/> オブジェクトを <see cref="ShaderProgram"/> オブジェクトにセットします｡
    /// <c>Attribute</c> 変数の詳細については､<see cref="VetexBuffer"/> の解説を参照してください。
    /// 
    /// </para>
    /// <para>
    /// <h3>頂点フィードバック</h3>
    /// この項目は高度な内容を含んでいます｡頂点フィードバックは <see cref="VertexShader"/> の出力を <see cref="FragmentShader"/> に送らずに
    /// フィードバック バッファーに横取りします｡
    /// この機能は画像を出力するのではなく頂点データを GPU を使って変更する場合などに使用ます｡
    /// 頂点フィードバックを有効にするには <see cref="ShaderProgram.SetFeedbackVarying"/> によって頂点フィードバック機能を有効にしたあと､
    /// 結果を受け取る<see cref="FeedbackBuffer"/> オブジェクトをセットします｡
    /// 頂点フィードバックの詳細については､<see cref="FeedbackBuffer"/> の解説を参照してください。
    /// 頂点フィードバックが有効な場合は <see cref="FragmentShader"/> をセットしなくてもエラーになりません(セットしても安全に無視されます)｡
    /// </para>
    /// <note type="caution">
    /// 2013年現在､NVIDIAのカードでフィードバック変数で指定した名前がドライバー内部で文字化けし､この機能を利用できない問題を確認しています｡
    /// 解決方法は現在の所ありません｡はよ直して･･･
    /// </note>
    /// <note type="implement">
    /// <see cref="VertexShader"/> と <see cref="FragmentShader"/> は削除して <see cref="ShaderProgram"/> に一本化する可能性が高い｡
    /// この2つを独立して存在させるメリットは特にない｡
    /// 頂点フィードバック機能はもう少し工夫の余地がある｡
    /// </note>
    /// <note type="implement">
    /// <c>Varying</c> 変数の一覧って取得できたっけ? できるなら<c>Attribute</c> 変数や<c>Uniform</c> 変数と同様なインターフェイスで
    /// 宣言を取得できた方が良いと思う｡
    /// </note>
    /// </remarks>
    /// <seealso cref="VertexShader"/>
    /// <seealso cref="FragmentShader"/>
    /// <seealso cref="ShaderDeclaration"/>
    /// <seealso cref="UniformArrays"/>
    /// <seealso cref="VertexBuffer"/>
    /// <seealso cref="FeedbackBuffer"/>
    /// <seealso cref="DDD.OpenTK.ShaderProgramExtensions"/>
    public class ShaderProgram : Object {

        #region Field
        VertexShader vertShader;
        FragmentShader fragShader;
        ShaderDeclaration[] attributes;
        ShaderDeclaration[] uniforms;
        string[] feedbacks;
        string log;
        #endregion


        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// 指定の頂点シェーダーとフラグメント シェーダーを持つシェーダー プログラム オブジェクトを作成します。
        /// この段階ではコンパイルとリンクは行われません｡シェーダーの妥当性は検証できません｡
        /// この段階で宣言された<c>Uniform</c> 変数と <c>Attribute</c> 変数を取得する事はできません｡
        /// 頂点フィードバックを使用する時はフラグメント シェーダーは省略可能で <c>null</c> を指定します。
        /// 初期値は､
        /// <list type="bullet">
        ///  <item><c>Uniform</c> 変数 - なし(取得前)</item>
        ///  <item><c>Attribute</c> 変数 - なし(取得前)</item>
        ///  <item><c>Varrying</c> 変数 - なし(取得前)</item>
        ///  <item>リンク ログ - なし(取得前)</c></item>
        /// </list>
        /// </remarks>
        /// <param name="vert"><see cref="VertexShader"/> オブジェクト(null不可)</param>
        /// <param name="frag"><see cref="FragmentShader"/> オブジェクト(null可)</param>
        /// <exception cref="ArgumentNullException"><paramref name="vert"/> が <c>null</c></exception>
        public ShaderProgram (VertexShader vert, FragmentShader frag) {
            if (vert == null) {
                throw new ArgumentNullException ("Vertex shader is null");
            }
            this.vertShader = vert;
            this.fragShader = frag;
            this.attributes = new ShaderDeclaration[0];
            this.uniforms = new ShaderDeclaration[0];
            this.feedbacks = new string[0];
            this.log = "";
        }

        #endregion

        #region Property

        /// <summary>
        /// バーテックス シェーダー
        /// </summary>
        /// <remarks>
        /// このシェーダー プログラムにセットされている <see cref="VertexSahder"/> オブジェクト
        /// </remarks>
        public VertexShader VertexShader {
            get { return vertShader; }
        }

        /// <summary>
        /// フラグメント シェーダー
        /// </summary>
        /// <remarks>
        /// このシェーダー プログラムにセットされている <see cref="FragmentSahder"/> オブジェクト
        /// </remarks>
        public FragmentShader FragmentShader {
            get { return fragShader; }
        }

        /// <summary>
        /// シェーダー プログラム中で宣言された <c>Attribute</c> 変数の総数
        /// </summary>
        /// <remarks>
        /// 頂点シェーダーで宣言された <c>Attribute</c> 変数の総数です｡
        /// コンパイル＆リンクを行うまでは0が返ります。
        /// <note type="tip">
        /// コンパイラの最適化によって使用されていない変数が削除されている事があります｡
        /// </note>
        /// </remarks>
        public int AttributeCount {
            get { return attributes.Length; }
        }

        /// <summary>
        /// シェーダー プログラム中で宣言された <c>Uniform</c> 変数の総数
        /// </summary>
        /// <remarks>
        /// 頂点シェーダー､およびフラグメント シェーダーの中で宣言された <c>Uniform</c> 変数の総数です｡
        /// コンパイル＆リンクを行うまでは0が返ります。
        /// <note type="tip">
        /// コンパイラの最適化によって使用されていない変数が削除されている事があります｡
        /// </note>
        /// </remarks>
        public int UniformCount {
            get { return uniforms.Length; }
        }

        /// <summary>
        /// フィードバック変数として使用する予定の <c>Varying</c> 変数の総数
        /// </summay>
        /// <remarks>
        /// フィードバック変数として使用する予定の <c>Varying</c> 変数の総数です｡
        /// 
        /// </remarks>
        public int FeedbackVaryingCount {
            get { return feedbacks.Length; }
        }

        /// <summary>
        /// フィードバックを使用するかどうかのフラグ
        /// </summary>
        /// <remarks>
        /// 頂点フィードバックを使用する場合は<c>true</c>, そうでない場合は<c>false</c>が返ります｡
        /// </remarks>
        public bool IsFeedbackEnabled {
            get { return (feedbacks.Length > 0) ? true : false; }
        }

        /// <summary>
        /// 宣言されているすべての <c>Attribute</c> 変数を列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 頂点シェーダーで定義されたすべての<c>Attribute</c> 変数を列挙します｡
        /// コンパイル＆リンクを行うまでは列挙できません。
        /// <note type="implement">
        /// コンパイラの最適化によって使用されていない変数が削除されている場合があります｡
        /// </note>
        /// 
        /// </remarks>
        public IEnumerable<ShaderDeclaration> Attributes {
            get { return attributes; }
        }

        /// <summary>
        /// 宣言されているすべての <c>Uniform</c> 変数を列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 頂点シェーダーで定義されたすべての<c>Uniform</c> 変数を列挙します｡
        /// コンパイル＆リンクを行うまでは列挙できません。
        /// <note type="implement">
        /// コンパイラの最適化によって使用されていない変数が削除されている場合があります｡
        /// </note>
        /// </remarks>
        public IEnumerable<ShaderDeclaration> Uniforms {
            get { return uniforms; }
        }

        /// <summary>
        /// フィードバック変数として使用する予定のすべての <c>Varying</c> 変数を列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 頂点フィードバック変数として使用される <c>Varying</c> 変数を列挙します｡
        /// 列挙子は
        /// </remarks>
        public IEnumerable<string> FeedbackVaryings {
            get { return feedbacks.Where (x => x != null); }
        }



        /// <summary>
        /// リンク ログ
        /// </summary>
        /// シェーダーをリンクした時のリンク ログです｡コンパイル前は空の文字列が返ります｡
        /// 返される文字列はドライバーに依存し､かつ予告無く変更される場合があります｡
        /// リンクが成功したか失敗したかを判定する目的で､このログを使用しないでください｡
        /// このログはデバッグを用途を想定しています｡
        public string LinkLog {
            get { return log; }
        }

        /// <inheritdoc/>
        public override IEnumerable<Object> References {
            get {
                var objs = base.References.ToList ();
                objs.Add (vertShader);
                objs.Add (fragShader);
                objs.RemoveAll (x => x == null);
                return objs;
            }
        }
        #endregion

        #region Method

        /// <summary>
        /// 指定の名前の <c>Attribute</c> 変数宣言を取得します
        /// </summary>
        /// <remarks>
        /// コンパイル＆リンクを行うまでは取得できません。
        /// 大文字小文字を厳密に判定します｡
        /// <note type="tip">
        /// コンパイラの最適化によって使用されていない変数が削除されている場合があります｡
        /// </note>
        /// </remarks>
        /// <param name="name">変数名</param>
        /// <exception cref="ArgumentNullException"><paramref name="name"/> が <c>null</c></exception>
        /// <exception cref="InvalidOperationException">リンク前の状態で呼ばれた</exception>
        /// <exception cref="ArgumentException">一致する名前の<c>Attribute</c> 変数が宣言されていない</exception>
        /// <returns>一致するシェーダー変数宣言</returns>
        /// <seealso cref="AttributeCount"/>
        public ShaderDeclaration GetAttribute (string name) {
            if (name == null || name == "") {
                throw new ArgumentNullException ("Name is null or empty");
            }
            if (attributes.Length == 0) {
                throw new InvalidOperationException ("No attributes was found (not linked?)");
            }
            if (!attributes.Any (x => x.Name == name)) {
                throw new ArgumentException ("Such attribute name was not found");
            }

            return attributes.First (x => x.Name == name);
        }

        /// <summary>
        /// 指定の名前の <c>Uniform</c> 変数宣言を取得します
        /// </summary>
        /// <remarks>
        /// 頂点シェーダー､およびフラグメント シェーダー中で宣言された<c>Uniform</c> 変数を取得します｡
        /// コンパイル＆リンクを行うまでは取得できません。
        /// 大文字小文字を厳密に判定します｡
        /// <note type="tip">
        /// コンパイラの最適化によって使用されていない変数が削除されている場合があります｡
        /// </note>
        /// </remarks>
        /// <param name="name">変数名</param>
        /// <exception cref="ArgumentNullException"><paramref name="name"/> が <c>null</c></exception>
        /// <exception cref="InvalidOperationException">リンク前の状態で呼ばれた</exception>
        /// <exception cref="ArgumentException">一致する名前の<c>Uniform</c> 変数が宣言されていない</exception>
        /// <returns>一致するシェーダー変数宣言</returns>
        /// <seealso cref="UniformCount"/>
        public ShaderDeclaration GetUniform (string name) {
            if (name == null || name == "") {
                throw new ArgumentNullException ("Name is null or empty");
            }
            if (uniforms.Length == 0) {
                throw new InvalidOperationException ("No Uniforms was found (not linked?)");
            }
            if (!uniforms.Any (x => x.Name == name)) {
                throw new ArgumentException ("Such uniforms name was not found");
            }
            return uniforms.First (x => x.Name == name);
        }


        /// <summary>
        /// リンク ログの変更
        /// </summary>
        /// <remarks>
        /// リンク ログをセットします｡
        /// </remarks>
        /// <param name="log">ログ</param>
        /// 
        internal void SetLog (string log) {
            this.log = log;
        }

        /// <summary>
        /// <c>Attribute</c> 変数宣言のセット
        /// </summary>
        /// <remarks>
        /// <c>Attribute</c> 変数宣言を一括してセットします｡
        /// 配列は内部に確保したメモリーにコピーされます｡
        /// </remarks>
        /// <param name="attributes"><c>Attribute</c> 変数宣言の配列</param>
        internal void SetAttributes (ShaderDeclaration[] attributes) {
            this.attributes = (ShaderDeclaration[])attributes.Clone ();
        }

        /// <summary>
        /// <c>Uniform</c> 変数宣言のセット
        /// </summary>
        /// <remarks>
        /// <c>Uniform</c> 変数宣言を一括してセットします｡
        /// 配列は内部に確保したメモリーにコピーされます｡
        /// </remarks>
        /// <param name="uniforms"><c>Uniform</c> 変数宣言の配列</param>
        internal void SetUniforms (ShaderDeclaration[] uniforms) {
            this.uniforms = (ShaderDeclaration[])uniforms.Clone ();
        }

        /// <summary>
        /// トランスフォーム フィードバック機能を有効にします
        /// </summary>
        /// <remarks>
        /// 頂点フィードバック(トランスフォーム フィードバック)の機能を有効にします｡<paramref name="count"/> には横取りする
        /// <c>Varying</c> 変数の最大数を指定します｡
        /// 頂点フィードバックの詳細については､<see cref="ShaderProgram"/> の解説を参照してください。
        /// </remarks>
        /// <param name="enable">有効・無効</param>
        /// <param name="count">使用する<c>Varying</c> 変数の総数</param>
        /// <exception cref="ArgumentException"><paramref name="count"/> &lt;= 0 または <paramref name="count"/> &gt; 128</exception>
        public void SetTransformFeedbackEnable (bool enable, int count) {
            if (!enable) {
                this.feedbacks = new string[0];
                return;
            }
            if (count <= 0 || count > 128) {
                throw new ArgumentException ("Count is invalid");
            }

            this.feedbacks = new string[count];
        }

        /// <summary>
        /// 頂点フィードバックで横取りする <c>Varying</c> 変数のセット
        /// </summary>
        /// <remarks>
        /// 頂点フィードバックで使用する <c>Varying</c> 変数を指定します｡
        /// <see cref="SetTransformFeedbackEnable"/> で機能を有効にした後､このメソッドを使用して
        /// 横取りする <c>Varying</c> 変数を指定してください｡
        /// 現在の実装では <see cref="FeedbackVaringCount"/> 個すべて指定してください｡
        /// 途中セットされていないインデックスがある場合の動作は未定義です｡
        /// <note type="implement">
        /// この辺の実装は正直微妙｡<see cref="ShaderProgram"/> に一本化ついでにもう少し整理する予定
        /// </note>
        /// </remarks>
        /// <param name="index">インデックス</param>
        /// <param name="name"><c>Varying</c> 変数名</param>
        /// <exception cref="IndexOutOfRangeException"><paramref name="index"/> &lt; 0 または <paramref name="index"/> &gt; <see cref="FeedbackVaryingCount"/></exception>
        /// <exception cref="name"><paramref name="name"/> は <c>null</c> または空文字列</exception>
        /// <exception cref="ArgumentException"><paramref name="name"/> はすでに登録済み</exception>
        /// <seealso cref="FeedbackBuffer"/>
        public void SetFeedbackVarying (int index, string name) {
            if (index < 0 || index >= FeedbackVaryingCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            if (name == null || name == "") {
                throw new ArgumentNullException ("Name is null or empty");
            }
            if (feedbacks.Contains (name)) {
                throw new ArgumentException ("Name is already registered");
            }

            this.feedbacks[index] = name;
        }

        /// <summary>
        /// 頂点フィードバックで使用する <c>Varying</c> 変数の削除
        /// </summary>
        /// <remarks>
        /// 頂点フィードバックで使用する <c>Varying</c> 変数を削除します｡
        /// 指定の名前が登録されていなかった場合は単にこれを無視します｡
        /// </remarks>
        /// <param name="name"><c>Varying</c> 変数名</param>
        /// <exception cref="ArgumentNullException"><paramref name="name"/> が <c>null</c> または空文字列</exception>
        /// <returns>削除したら<c>true</c>, そうでなければ<c>false</c></returns>
        public bool RemoveFeedbackVarying (string name) {
            if (name == null || name == "") {
                throw new ArgumentNullException ("Name is null or empty");
            }
            for (var i = 0; i < FeedbackVaryingCount; i++) {
                if (feedbacks[i] == name) {
                    this.feedbacks[i] = null;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// フィードバック <c>Varying</c> 変数の取得
        /// </summary>
        /// <remarks>
        /// 指定のインデックスのフィードバック<c>Varying</c>変数を取得します｡
        /// </remarks>
        /// <param name="index">インデックス</param>
        /// <exception cref="IndexOutOfRangeException"><paramref name="index"/> &lt; 0 または <paramref name="index"/> &gt;= <see cref="FeedbackBaringCount"/></exception>
        /// <returns><c>Varying</c>変数の文字列</returns>
        public string GetFeedbackVarying (int index) {
            if (index < 0 || index >= FeedbackVaryingCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }

            return feedbacks[index];
        }


        /// <inheritdoc/>
        public override string ToString () {
            return string.Format ("ShaderProgram:  Vert={1}, Frag={2}", VertexShader, FragmentShader);
        }
        #endregion

    }
}
